並行編程在現代軟體開發中變得越來越重要,特別是在高效能應用程式中,如何充分利用多核處理器的計算能力至關重要。Rust 作為一個系統程式設計語言,以其獨特的記憶體安全機制和所有權系統,提供了安全且高效的並行程式設計方式。這篇文章將探討 Rust 中的兩種並行編程技術:threads 和 async,並展示如何利用這些技術來編寫高效能的並行程式。
並行編程是指讓程式同時執行多個操作或計算的技術。這通常涉及在多個執行緒(threads)上同時運行代碼,以提高性能和資源利用效率。在現代的多核處理器上,並行編程允許應用程式充分利用多個 CPU 核心的計算能力。
Rust 透過兩種主要方式來支持並行編程:
Threads(執行緒):想像你在家裡有兩個人同時在做家事。一個人在打掃客廳,另一個人在洗碗。兩個人各做各的,互不干涉,但每個人都會專心完成自己的任務,直到做完為止。這種方式就是 Threads(執行緒),每個人都是一個執行緒,可以同時進行不同的工作。
Async(非同步):非同步比較像是你一個人在處理多個任務,但是你很會安排事情的先後順序,知道哪些事情可以等、哪些事情需要先做。比如你開始煮飯的時候,煮飯需要時間等待,你就不會乾等著,會利用這段時間去洗碗或摺衣服。等煮飯好了,你再回來看看飯煮得怎麼樣。這就是 Async(非同步),它允許你在等待某件事的時候,去做其他不需要等的事情,讓工作效率提高。
Rust 的執行緒使用標準函式庫中的 std::thread
模組來創建。Rust 的執行緒模型非常高效,並且透過所有權系統確保了資料的安全共享。
你可以使用 thread::spawn
函數來創建一個新的執行緒。以下是一個簡單的範例,展示如何創建兩個執行緒,並讓它們同時執行不同的操作:
use std::thread; // 引入 thread 模組,用於創建執行緒
use std::time::Duration; // 引入 Duration 模組,用於設定時間長度
fn main() {
// 創建一個執行緒,並在執行緒中打印訊息
let handle = thread::spawn(|| {
for i in 1..5 { // 執行緒中的迴圈,從 1 執行到 4
println!("執行緒 1:數字 {} ", i); // 打印執行緒中的數字
thread::sleep(Duration::from_millis(500)); // 暫停 500 毫秒
}
});
// 主執行緒同時執行
for i in 1..3 { // 主執行緒中的迴圈,從 1 執行到 2
println!("主執行緒:數字 {} ", i); // 打印主執行緒中的數字
thread::sleep(Duration::from_millis(500)); // 暫停 500 毫秒
}
// 等待執行緒完成
handle.join().unwrap(); // 等待子執行緒執行完畢後再繼續
println!("全部執行完畢");
}
主執行緒:數字 1
執行緒 1:數字 1
主執行緒:數字 2
執行緒 1:數字 2
執行緒 1:數字 3
執行緒 1:數字 4
全部執行完畢
handle.join()
是用來確保主執行緒等待新執行緒完成的方式。如果你不使用 join
,主執行緒可能在新執行緒完成之前就結束了,導致你無法看到新執行緒的輸出。Duration::from_millis()
是 Rust 標準庫中的一個方法,用於創建一個 Duration
物件,代表以毫秒為單位的時間長度。這個方法在 Rust 的並行和異步編程中經常用來設定等待時間或計時。在多執行緒的程式設計中,讓多個執行緒能夠共用資料是很常見的需求,但這樣做也可能會引發「搶著使用」的問題,導致資料不一致或錯誤。Rust 使用它特有的所有權系統和安全的並行編程工具,來幫助我們解決這些問題,確保多個執行緒在共享資料時是安全的、不會出錯。
Arc
和 Mutex
共享資料如果你想在多個執行緒間共享資料,通常會使用 Arc
(原子引用計數)和 Mutex
(互斥鎖)來實現安全的資料共享。以下範例展示了如何使用 Arc
和 Mutex
在多個執行緒間共享一個可變變數。
use std::sync::{Arc, Mutex}; // 引入 Arc 和 Mutex,用於安全共享資料
use std::thread; // 引入 thread 模組,用於創建執行緒
fn main() {
// 創建一個共享的計數器,使用 Arc 和 Mutex 進行保護
let counter = Arc::new(Mutex::new(0));
let mut handles = vec![]; // 創建一個空的向量來存儲執行緒的 handle
// 創建 10 個執行緒,每個執行緒都會增加計數器
for _ in 0..10 {
let counter = Arc::clone(&counter); // 克隆 Arc,讓每個執行緒都能共享同一個計數器
let handle = thread::spawn(move || { // 創建一個新執行緒
let mut num = counter.lock().unwrap(); // 獲取 Mutex 的鎖,並安全地訪問計數器
*num += 1; // 增加計數器的值
});
handles.push(handle); // 將執行緒的 handle 添加到向量中
}
// 等待所有執行緒完成
for handle in handles {
handle.join().unwrap(); // 等待每個執行緒完成執行
}
// 顯示最終的計數值
println!("最終計數器:{}", *counter.lock().unwrap()); // 獲取 Mutex 的鎖,並打印計數器的值
}
最終計數器:10
Arc
(Atomic Reference Counted)Arc
是一個智能指標,允許多個執行緒安全地共享相同的資料。它使用原子操作來管理內部的引用計數,確保同一時間可以安全地被多個執行緒引用。Arc
透過增加和減少引用計數來管理內存的所有權。當引用計數變為 0 時,資料會被釋放。Mutex
來實現可變的共享。Mutex
(Mutual Exclusion)Mutex
是一種互斥鎖,用來保護共享的可變資料,確保同一時間只有一個執行緒能夠訪問這些資料。Mutex
的鎖。當一個執行緒獲得鎖後,其他執行緒將被阻塞,直到鎖被釋放。在下面的範例中,我們創建了一個 Arc<Mutex<i32>>
,這樣做的目的是讓多個執行緒可以安全地共享一個計數器,並確保每次訪問和修改計數器時,只有一個執行緒能夠進行操作。
use std::sync::{Arc, Mutex};
use std::thread;
fn main() {
// 創建一個共享的計數器,使用 Arc 和 Mutex 保護
let counter = Arc::new(Mutex::new(0));
let mut handles = vec![];
// 創建 10 個執行緒,每個執行緒都會增加計數器
for _ in 0..10 {
let counter = Arc::clone(&counter); // 克隆 Arc 以便執行緒共享
let handle = thread::spawn(move || {
let mut num = counter.lock().unwrap(); // 獲取 Mutex 鎖,安全訪問數據
*num += 1; // 增加計數器
});
handles.push(handle);
}
// 等待所有執行緒完成
for handle in handles {
handle.join().unwrap();
}
// 顯示最終的計數值
println!("最終計數器:{}", *counter.lock().unwrap());
}
Threading
和 Lock
在 Python 中,類似的功能可以通過 threading
模組中的 Lock
來實現。以下是 Python 版本的程式碼,用於演示如何在多執行緒之間安全地共享資料:
import threading
# 創建一個全域變數計數器和一個鎖
counter = 0
counter_lock = threading.Lock()
def increment_counter():
global counter
with counter_lock: # 獲取鎖
counter += 1 # 安全地增加計數器
threads = []
# 創建 10 個執行緒
for _ in range(10):
thread = threading.Thread(target=increment_counter)
threads.append(thread)
thread.start()
# 等待所有執行緒完成
for thread in threads:
thread.join()
print(f"最終計數器:{counter}")
Arc
來共享資料,而 Python 使用全域變數或物件。Mutex
確保安全訪問,而 Python 使用 Lock
進行互斥控制。Mutex
會強制處理錯誤(例如解鎖失敗時),而 Python 的 Lock
相對簡單,但對於資料一致性和安全性的控制較弱。Mutex
在效能上通常優於 Python 的 Lock
,特別是在高並發環境中,由於 Rust 更加接近底層和無 GIL 限制。Rust 的異步編程模型主要基於 Future
和 async/await
,這讓 Rust 能夠非常高效地處理需要等待的操作,而不必浪費執行緒的資源。當程式遇到需要等待的事情(例如網路請求、文件讀取等),它可以選擇繼續做其他任務,直到前面的操作完成時再回來。這讓程式運作更加靈活和有效率。
async
語法要使用 Rust 的非同步功能,可以用 async
關鍵字來定義非同步函數,並用 await
來等待這些函數完成。下面的範例展示了如何在 Rust 中運行非同步任務,並對其行為進行更直觀的解釋。
首先引入 tokio
套件:
在 Cargo.toml
檔案中加入以下內容來安裝 tokio
,這是 Rust 中常用的非同步運行時:
[dependencies]
tokio = { version = "1", features = ["full"] }
設定完成後,記得執行 cargo build
來安裝這個套件。
非同步程式碼範例說明:
use std::time::Duration;
use tokio::time::sleep;
// 定義一個非同步任務 async_task
async fn async_task(id: u32) {
println!("非同步任務 {} 開始", id); // 顯示任務開始
sleep(Duration::from_secs(2)).await; // 非同步等待 2 秒
println!("非同步任務 {} 完成", id); // 顯示任務完成
}
#[tokio::main]
async fn main() {
println!("呼叫非同步任務");
// 創建多個非同步任務並同時執行
let task1 = async_task(1);
let task2 = async_task(2);
// 使用 tokio::join! 同時運行兩個非同步任務
tokio::join!(task1, task2);
println!("所有非同步任務已完成");
// 這部分是主執行緒的同步操作
for i in 1..=3 {
println!("主執行緒的同步操作 {}", i);
tokio::time::sleep(Duration::from_millis(500)).await; // 模擬主執行緒的操作
}
}
定義非同步任務 (async_task
):
async fn async_task(id: u32)
:定義了一個非同步函數,該函數等待 2 秒後顯示任務完成。sleep(Duration::from_secs(2)).await
:使用 await
等待操作完成,這期間可以讓其他非同步任務繼續執行。執行非同步任務:
#[tokio::main]
:這個標籤告訴 Rust 使用 tokio
來運行這個非同步的 main
函數。tokio::join!(task1, task2)
:同時執行 task1
和 task2
,並等待這兩個任務完成,然後繼續下一步操作。主執行緒的同步操作:
實際輸出顯示非同步任務是先完成,然後再進行主執行緒的同步操作。這證明了 tokio::join!
是等待所有非同步任務結束後才進行下一步的操作:
呼叫非同步任務
非同步任務 1 開始
非同步任務 2 開始
非同步任務 2 完成
非同步任務 1 完成
所有非同步任務已完成
主執行緒的同步操作 1
主執行緒的同步操作 2
主執行緒的同步操作 3
這段程式碼展示了非同步任務如何同時執行,提升了程式在處理多個操作時的效率。實際輸出結果顯示,非同步任務在幾乎同一時間開始並且互不干涉,直到所有任務完成後,程式才會繼續進行後續的同步操作。
從結果可以看到,tokio::join!
讓兩個非同步任務同時開始執行,並等到它們都完成後,才會繼續執行主執行緒的操作,通常這樣的設計是用於需要等待外部資源或者特定函數程序處理完畢之後才能繼續下一步的情境。
threads
與 async
?Threads:適合 CPU 密集型任務或需要並行處理的場景,例如大量計算或處理大型資料集。執行緒是並行運行的,適合多核 CPU 的應用。
Async:適合 IO 密集型任務,例如網路請求、文件讀寫等,這些任務通常會等待外部資源完成。非同步編程允許單個執行緒處理多個任務,而不需要創建大量執行緒。
在這裡,我們透過一個有趣的小測試來比較 Python 和 Rust 在多執行緒處理上的效能表現。測試的內容非常簡單:我們讓兩種語言各自進行大量的計算任務——找出 50,000 以內的素數。素數計算是一種典型的 CPU 密集型工作,適合用來測試多執行緒的效能。
Python 的範例使用了兩種方法:多線程 和 多進程。在 Python 中,由於 GIL(全域直譯器鎖)的限制,多線程在 CPU 密集型任務中不能完全發揮效能,所以我們也使用 multiprocessing
來創建多個獨立的進程,看看它是否會更快一些。
import threading # 引入 threading 模組,用於創建和操作執行緒
import time # 引入 time 模組,用於計算執行時間
from multiprocessing import Pool # 引入 multiprocessing 模組中的 Pool,用於多進程處理
# 定義一個計算素數的函數
def count_primes(n):
count = 0 # 計數器,初始化為 0
for i in range(2, n): # 從 2 開始迭代到 n-1
for j in range(2, int(i ** 0.5) + 1): # 檢查從 2 到該數字平方根的所有數
if i % j == 0: # 如果能整除,則不是素數
break
else:
count += 1 # 如果沒被 break 中斷,則是素數,計數器加 1
return count # 返回素數的個數
# 定義一個執行緒的函數
def run_threaded():
count_primes(50000) # 計算 50000 以內的素數
if __name__ == '__main__':
# 使用 threading 的範例
threads = [threading.Thread(target=run_threaded) for _ in range(2)] # 創建兩個執行緒,每個執行 run_threaded
start_time = time.time() # 記錄開始時間
# 啟動執行緒
for thread in threads:
thread.start() # 啟動每個執行緒
# 等待執行緒完成
for thread in threads:
thread.join() # 等待每個執行緒結束
elapsed_time = (time.time() - start_time) * 1000 # 計算執行時間,並轉換為毫秒
print(f"Threading - 執行時間:{elapsed_time:.2f} 毫秒") # 打印 threading 的執行時間
# 使用 multiprocessing 的範例
start_time = time.time() # 記錄開始時間
with Pool(2) as pool: # 創建兩個進程的池
pool.map(count_primes, [50000, 50000]) # 並行計算兩次 count_primes,參數各為 50000
elapsed_time = (time.time() - start_time) * 1000 # 計算執行時間,並轉換為毫秒
print(f"Multiprocessing - 執行時間:{elapsed_time:.2f} 毫秒") # 打印 multiprocessing 的執行時間
接著,我們來看 Rust 的版本。Rust 使用 std::thread
來進行多執行緒運算,由於沒有 GIL 的限制,Rust 的執行緒可以完全並行地工作,充分利用多核 CPU 的威力。
use std::thread;
use std::time::Instant;
// 計算 50,000 以內的素數
fn count_primes(n: u64) -> u64 {
let mut count = 0;
for i in 2..n {
if (2..=((i as f64).sqrt() as u64)).all(|j| i % j != 0) {
count += 1;
}
}
count
}
fn main() {
let start_time = Instant::now();
// 創建兩個執行緒,並行計算
let handles: Vec<_> = (0..2)
.map(|_| {
thread::spawn(|| {
count_primes(50000);
})
})
.collect();
for handle in handles {
handle.join().unwrap();
}
println!("Rust - 執行時間:{:.2?} 毫秒", start_time.elapsed());
}
執行這些程式後,我們得到以下結果:
Python 的執行結果:
Threading - 執行時間:89.66 毫秒
Multiprocessing - 執行時間:207.09 毫秒
可以看到,Python 的多線程效能雖然不錯,但仍受到 GIL 的影響,無法完全釋放 CPU 的力量;而多進程雖然繞過了 GIL,但建立和管理進程的成本較高,因此耗時更多。
Rust 的執行結果:
Rust - 執行時間:14.84 毫秒
Rust 的表現非常驚人,運行速度大幅超越 Python,這主要歸功於 Rust 的多執行緒可以真正並行運行,而不會受到 GIL 限制。Rust 的編譯器優化和系統級效能,使得這類 CPU 密集型工作如魚得水。
對於習慣使用 Python 的開發者來說,當面對大量資料處理或高效能需求時,經常會感受到 Python 的不足。即使使用多線程或多進程技術提升效能,仍難以突破 GIL 的瓶頸。但透過這篇文章的比較,我們可以清楚看到 Rust 在多執行緒應用上的顯著優勢。
這不僅僅是效能的提升,更代表著開發潛力和用戶體驗的進一步突破。Rust 的記憶體安全性、所有權系統、以及無與倫比的執行效能,讓它成為高效能應用程式開發的強力選擇。
看到這些成果,相信你和我一樣感到興奮!接下來的日子,我們將繼續探索 Rust 為 Python 開發者帶來的更多驚喜——15 天過去了,Rust還有哪些值得Python開發者們探索的地方呢?就讓我們繼續看下去~